***** ***** by crUsAdEr ***** ****
This tutorial aims to discuss about PElock 1.06 protection. Various techniques are employed to prevent crackers from fooling with this protector, so in this tutorial I hope to reverse some interesting features of PELock and eventually remove the protection layer. PELock is highly OS dependent and this tutorial is done purely on win2k so some protection features discussed here will be different on other OS.
TOOLS used :
IDA 4.15
Soft Ice on Win2k SP3
LordPE Deluxe
Superbpm for NT/2k
Imprec 1.4.2
Targets : PELock 1.06
Included : PElock.idc
Prologue
PELock is a very interesting protection with many features similar to SVKProtector especially IAT redirection, code decryption after OEP. PELock is also able to detect icedump on win98 by exploiting the fact that icedump leaves kernel32.dll memory unprotected (as discussed by SplAj on Fravia Message Board). Though I try to make this essay as reader-friendly as possible, I assume that readers have some background in unpacking, have thorough knowledge of Structured Exception Handling and are able to use the tools listed above comfortably. PELock does extensive bpx checking by checking opcodes for "CC" so throughout this exercise, do not use bpx on API or PELock will throw a tantrum and exit silently. Also, PELock clears debug register with via SEH so you should have SuperBPM running at all time.
(Finally, though it is not essential, I recommend readers to read my two previous essays on AsProtect 1.2 and Armadillo 2.6 to gain some background knowledge as I will not be repeating the same points covered in those essays.)
1. Playing with the lock
OK, I hope you have already downloaded PELock and discovered that it wont run together with softice. We will worry about it later! The heart of PELock or as many other protector, lies in its loader that performs the target file decryption, import loading and we will set out obtaining a valid dump of PELock loader first. Load Superbpm for NT/2k with softice and do "bpm VirtualAlloc x". Run PELock, once softice breaks, you will land here
00431682 push 4 00431684 push 3000h 00431689 push 0F000h ; loader size 0043168E push ecx 0043168F mov edx, [ebp+VirtualAlloc] 00431692 cmp byte ptr [edx], 0CCh ; check opcodes for bpx 00431695 setz cl ; if bpx found then 00431698 add edx, ecx ; change value of edx and causes error 0043169A call edx 0043169C lea edx, [ebp+756h] <=== YOU SHOULD LAND HERE 004316A2 push eax ; here eax is the address of VirtualAlloc memory 004316A3 push edx ; where the main PELock loader will be decrypted to 004316A4 push eax 004316A5 pusha <-----------snip-------------> <--loader decryption routine--> 00431768 mov [esp+1Ch], edi 0043176C popa 0043176D retn ; jumps to PELock loader code from here
As you have seen above, this is where PELock loader is decrypted into memory. The long decryption routine is removed but you can simply scroll down in softice windows to find the "ret" instruction. We need to put a break point at this address 43176D and make a dump of this PELock loader, you can look in stack to find out the base address of our new decrypted loader. Once dumped, we can load this loader into IDA to disassemble and study. I trust you know how to do this by building a simple PE Header for our dump or cut and paste into PELock.exe and uses Reload Input File in IDA.
Once you manage to load PELock loader into IDA, you will see this:
00401000 call near ptr PE_Lock_start+24h 00401005 00401005 GetAPIAddress proc near ; CODE XREF: ... 00401005 00401005 var_4= byte ptr -4 00401005 arg_0= dword ptr 8 00401005 arg_4= dword ptr 0Ch 00401005 arg_8= dword ptr 10h 00401005 arg_C= byte ptr 14h 00401005 arg_104= dword ptr 10Ch 00401005 00401005 push ebp 00401006 mov ebp, esp 00401008 pusha
<---------continue---------->
The snippet above are a few first instruction of PELock loader, starting with a "home-made" GetProcAddress routine that read system dll PE Header and traverse its export directory to find API address. API names are hashed and compared with tables (we will discuss more about this in API redirection section). Once each API address is found, the first byte is checked against "CC" and if bpx is detected then the execution flow will be changed slightly causing PELock to eventually exit in silence. As the whole loader is not fully decrypted yet, various parts of the loader are still encrypted and would only be decrypted later as PELock proceed to loading its main exe so as you debug and run PElock, if you find that IDA disassembly is different from softice screen then you will need to make a new dump of the loader and paste the newly decrypted loader code over the encrypt code for IDA to disassemble them properly.
As you start tracing PELock loader, you will realize that there are a lot garbage code inserted in the loader to make tracing and disassembling difficult. Here are a few typical examples of garbage code:
0040159F C1 F7 00 sal edi, 0 004015A2 C1 F2 00 sal edx, 0 ; do nothing 004015A5 EB 02 jmp short loc_4015A9 ; jump over 004015A5 ; ----------------------------------------------------------- 004015A7 CD db 0CDh ; - 004015A8 20 db 20h ; 004015A9 ; ----------------------------------------------------------- 004015A9 004015A9 loc_4015A9: ; CODE XREF: ... 004015A9 C1 F0 00 sal eax, 0 004015AC EB 02 jmp short loc_4015B0 004015AC ; ----------------------------------------------------------- 004015AE C9 db 0C9h ; + 004015AF 22 db 22h ; " 004015B0 ; ----------------------------------------------------------- 004015B0 004015B0 loc_4015B0: ; CODE XREF: ... 004015B0 E8 01 00 00 00 call loc_4015B6 ; call over head 004015B0 ; ----------------------------------------------------------- 004015B5 42 db 42h ; B 004015B6 ; ----------------------------------------------------------- 004015B6 004015B6 loc_4015B6: ; CODE XREF: ... 004015B6 8D 64 24 04 lea esp, [esp+4] ; correct stack OR THIS 004015F1 E8 01 00 00 00 call loc_4015F7 004015F1 ; ----------------------------------------------------------- 004015F6 3B db 3Bh ; ; 004015F7 ; ----------------------------------------------------------- 004015F7 004015F7 loc_4015F7: ; CODE XREF: ... 004015F7 8F 44 24 FC pop dword ptr [esp-4] ; correct stack OR THIS 004015D6 7A 03 jp short loc_4015DB ; doesn't matter the status of EFlags 004015D8 7B 01 jnp short loc_4015DB ; one of these jumps will jump 004015D8 ; ----------------------------------------------------------- 004015DA 7E db 7Eh ; ~ 004015DB ; ----------------------------------------------------------- 004015DB 004015DB loc_4015DB: ; CODE XREF: ... 004015DB EB 02 jmp short loc_4015DF ; garbage code 004015DB ; ----------------------------------------------------------- 004015DD 8B db 8Bh ; ï 004015DE 8D db 8Dh ; ì 004015DF ; ----------------------------------------------------------- 004015DF 004015DF loc_4015DF: ; CODE XREF: ... 004015DF C1 F6 00 sal esi, 0
It is very important to remove these garbage codes that do nothing from our disassembly so that we can have a better over view of the loader codes as well as save us time from correcting the disassembly in IDA. You can try using various tools like MOW, Imhotep etc or code your own tool to clean out the garbage. I coded myself a simple idc script which checks the opcodes pattern for these garbage code and NOP them out, displaying them as single line array. Further code snippet of this tutorial are taken after most garbage code had been removed. I have attached my idc script above for reference purpose, you can try coding your own script to suit your need.
2. Softice detection
Once you have cleaned out most of the garbage code of PELock, you can then comfortably proceed on to study PELock but first we need PELock to run in the presence of Softice. Under win2k, PELock has too effective softice detection which is int1 detection and UnhandledExceptionFilter byte check.
When softice is loaded, softice replaces the first byte of kernel32!UnhandledExceptionFilter API with a "CC" instead of the usual "55". PELock checks this byte for "CC" as follow
004010D4 80 38 CC cmp byte ptr [eax], 0CCh ; '¦' 004010D7 75 0F jnz short OK 004010D9 74 01 jz short near ptr bad
Thus, to bypass this check, we place a memory READ breakpoint on
UnhandledExceptionFilter and change the Zero flag.
bpm UnhandledExceptionFilter r do "r fl z"
When Windows 2000 is running without softice, INT01 descriptor entry in IDT has DPL=0. So if a ring-3 task attempts to call INT01 with DPL=0, a GP=INT0D fault will occur and Windows INT0D handler will report C0000005 (EXCEPTION_ACCESS_VIOLATION) as the exception code and Eip as address where exception occur. When ntice is loaded, it sets DPL=3 for INT01 descriptor entry in IDT and now INT01 can be accessed by normal ring-3 task. Now if an INT01 is encountered, the next instruction address is stored on stack instead and Windows will check for any breakpoints placed, if not found then Windows will report a 80000004 (EXCEPTION_SINGLE_STEP) exception. In short, if softice is loaded, then the exception code is 80000004 and exception address is EIP+2 when int1 is encountered while if softice is not loaded, the exception code would be C0000005 and exception address would be EIP. (For detail reference, please refer to Intel Manual Volume 3). Thanks [Yates] for the explanation!
PELock uses the fact that exception addresses differ to check if softice is loaded. An normal SEH handler is set up!
004044A3 CD 01 int 1 ; exception address here if softice is not loader
004044A5 C3 retn ; exception address here if softice loaded 004044A6 ; ----------------------------------------------------------- 004044A6 EB 36 jmp short near sice_not_present 004044A6 ; ----------------------------------------------------------- 004044A8 90 90 90 90 90 90+ Softice_detected db 35h dup(90h) 004044DD ; ----------------------------------------------------------- 004044DD C3 retn 004044DD ; ----------------------------------------------------------- 004044DE 90 90 90 90 90 90+sice_not_present db 40h dup(90h) <--- Inside SEH handler --->
004040E4 83 A8 B8 00 00 00+ sub [eax+CONTEXT.Eip], 0FFFFFFFDh ; add Eip by 3 bytes
004040E4 FD ; if sice is present, int1 exception
004040E4 ; will move Eip ahead by 2 bytes already
Hence, if softice is detected, the execution flow will change and soon PELock
will exit silently. To overcome this detection, we need to patch IDT entry for
INT01 descriptor so that DPL=0 again. In win2k do
idt ; to
obtain address of Interrupt Descriptors Table
d <address of IDT>
Change the 0Dh byte from IDT on the table from EE to 8E
Thanks SiNTaX and Nikolatesla for the tip
For more detailed explanation, you can refer to Art Of Assembly about IDT format.
3. Import Rebuilding
Once you are done with IDT patching, UnhandedExceptionFilter will break twice but PELock will happily run with softice loaded. Our next task is then to obtain valid import data. Run PELock, at the first UnhandledExceptionFilter breakpoint, you will be inside the "home-made" GetProcAddress described above, do "bpm Eip x" to place a breakpoint in this routine, when softice breaks at this breakpoint, press F12 once and you will be in the middle of IAT loading and redirecting process.
00405350 EB 2A jmp short near ptr ImportLoadingStart 00405350 ; ------------------------------------------------------
00405352 90 90 90 90 90 90+next_dll_entry db 27h dup(90h) ; CODE XREF: seg000:004059C3j 00405379 ; ------------------------------------------------------ 00405379 8D 7E 01 lea edi, [esi+1] 00405379 ; ------------------------------------------------------ 0040537C 90 90 90 90 90 90+ImportLoadingStart db 24h dup(90h) ; CODE XREF: seg000:00405350j 004053A0 ; ------------------------------------------------------ 004053A0 57 push edi ; edi -> DLL name 004053A1 FF 55 00 call [ebp+API.LoadLibrary] 004053A1 ; ------------------------------------------------------ 004053A4 90 90 90 90 90 90+ db 2Ch dup(90h) 004053D0 ; ------------------------------------------------------ 004053D0 85 C0 test eax, eax 004053D2 0F 84 13 1A 00 00 jz Library_loading_fail 004053D2 ; ------------------------------------------------------ 004053D8 90 90 90 90 90 90+ db 26h dup(90h) 004053FE ; ------------------------------------------------------ 004053FE 8B D0 mov edx, eax ; edx = DLL handle 004053FE ; ------------------------------------------------------
00405400 90 90 90 90 90 90+ db 2Dh dup(90h) 0040542D ; ------------------------------------------------------ 0040542D 8B F7 mov esi, edi ; import data block pointer 0040542D ; ------------------------------------------------------ 0040542F 90 90 90 90 90 90+ db 2Dh dup(90h) 0040545C ; ------------------------------------------------------ 0040545C string_scan_loop: ; CODE XREF: seg000:00405461j 0040545C 8A 06 mov al, [esi] ; simple loop scan along Import Data block 0040545E 46 inc esi ; to mov pointer to the end of DLL name 0040545F 84 C0 test al, al ; by checking for null byte 00405461 75 F9 jnz short string_scan_loop 00405461 ; ------------------------------------------------------ 00405463 90 90 90 90 90 90+not_end_of_thunk db 4Eh dup(90h) ; CODE XREF: seg000:00405925j 004054B1 ; ------------------------------------------------------ 004054B1 8B 46 01 mov eax, [esi+1] ; get API name hash from Import Data Block 004054B1 ; ------------------------------------------------------ 004054B4 90 90 90 90 90 90+ db 2Ah dup(90h) 004054DE ; ------------------------------------------------------ 004054DE B9 B2 06 00 00 mov ecx, 6B2h ; initialize counter for CRC loop below 004054DE ; ------------------------------------------------------ 004054E3 90 90 90 90 90 90+ db 23h dup(90h) 00405506 ; ------------------------------------------------------ 00405506 CRC_import_decrypt_loop: ; CODE XREF: seg000:00405510j 00405506 2B 84 8D 0A 3D 00+ sub eax, [ebp+ecx*4+3D0Ah] ; loop decrypting API name hash 0040550D D3 C0 rol eax, cl ; using CRC of the code area around here 0040550F 49 dec ecx ; to prevent patching 00405510 75 F4 jnz short CRC_import_decrypt_loop 00405510 ; ------------------------------------------------------ 00405512 90 90 90 90 90 90+ db 28h dup(90h) 0040553A ; ------------------------------------------------------ 0040553A 8A 0E mov cl, [esi] ; get the first char of API name 0040553C 83 C6 05 add esi, 5 0040553C ; ------------------------------------------------------ 0040553F 90 90 90 90 90 90+ db 23h dup(90h) 00405562 ; ------------------------------------------------------ 00405562 51 push ecx ; push first_char 00405562 ; ------------------------------------------------------ 00405563 90 90 90 90 90 90+ db 23h dup(90h) 00405586 ; ------------------------------------------------------ 00405586 50 push eax ; push API_name_hash 00405586 ; ------------------------------------------------------ 00405587 90 90 90 90 90 90+ db 32h dup(90h) 004055B9 ; ------------------------------------------------------ 004055B9 52 push edx ; push DLL handle 004055B9 ; ------------------------------------------------------ 004055BA 90 90 90 90 90 90+ db 29h dup(90h) 004055E3 ; ------------------------------------------------------ 004055E3 FF 75 00 push dword ptr [ebp+0] 004055E3 ; ------------------------------------------------------ 004055E6 90 90 90 90 90 90+ db 33h dup(90h) 00405619 ; ------------------------------------------------------ 00405619 E8 E7 B9 FF FF call GetAPIAddress ; The home-made GetProcAddress 00405619 ; ------------------------------------------------------ 0040561E 90 90 90 90 90 90+ db 30h dup(90h) <=== YOU SHOULD LAND HERE
but these garbage code are removed by my idc script 0040564E ; ------------------------------------------------------ 0040564E 85 C0 test eax, eax ; check API address found 0040564E ; ------------------------------------------------------ 00405650 90 90 90 90 90 90+ db 1Dh dup(90h) 0040566D ; ------------------------------------------------------ 0040566D 0F 84 92 17 00 00 jz API_add_not_found 0040566D ; ------------------------------------------------------ 00405673 90 90 90 90 90 90+ db 5Bh dup(90h) 004056CE ; ------------------------------------------------------ 004056CE F6 C1 80 test cl, 80h ; check redirection flag 004056CE ; if cl=80 then API is to be redirected 004056CE ; ------------------------------------------------------ 004056D1 90 90 90 90 90 90+ db 2Bh dup(90h) 004056FC ; ------------------------------------------------------ 004056FC 8B 0E mov ecx, [esi] 004056FC ; ------------------------------------------------------ 004056FE 90 90 90 90 90 90+ db 47h dup(90h) 00405745 ; ------------------------------------------------------ 00405745 0F 84 87 00 00 00 jz near ptr redirect_import+2Bh ; our CRUCIAL JUMP 00405745 ; ------------------------------------------------------ 0040574B 90 90 90 90 90 90+ db 55h dup(90h) 004057A0 ; ------------------------------------------------------ 004057A0 89 01 mov [ecx], eax ; update First Thunk of Import Directory 004057A2 E9 1F 01 00 00 jmp next_entry_in_thunk ; with original API address 004057A2 ; ------------------------------------------------------ 004057A7 90 90 90 90 90 90+redirect_import db 8Bh dup(90h) ; CODE XREF: seg000:00405745j 00405832 ; ------------------------------------------------------ 00405832 89 19 mov [ecx], ebx ; update First Thunk with redirected API address 00405832 ; ------------------------------------------------------ 00405834 90 90 90 90 90 90+ db 5Dh dup(90h) 00405891 ; ------------------------------------------------------ 00405891 E8 51 01 00 00 call near ptr redirect_API+1Fh 00405891 ; ------------------------------------------------------ 00405896 90 90 90 90 90 90+ db 30h dup(90h) 004058C6 ; ------------------------------------------------------ 004058C6 next_entry_in_thunk: ; CODE XREF: seg000:004057A2j 004058C6 83 C6 04 add esi, 4 004058C6 ; ------------------------------------------------------ 004058C9 90 90 90 90 90 90+ db 30h dup(90h) 004058F9 ; ------------------------------------------------------ 004058F9 B1 FE mov cl, 0FEh ; '¦' ; check for stopping byte 004058F9 ; ------------------------------------------------------ 004058FB 90 90 90 90 90 90+ db 28h dup(90h) 00405923 ; ------------------------------------------------------ 00405923 38 0E cmp [esi], cl ; if found then it is the end of thunk 00405925 0F 85 60 FB FF FF jnz near ptr not_end_of_thunk+28h 00405925 ; ------------------------------------------------------ 0040592B 90 90 90 90 90 90+ db 32h dup(90h) 0040595D ; ------------------------------------------------------ 0040595D 38 4E 01 cmp [esi+1], cl ; check for stopping byte again 0040595D ; ------------------------------------------------------ 00405960 90 90 90 90 90 90+ db 28h dup(90h) 00405988 ; ------------------------------------------------------ 00405988 0F 84 FC 17 00 00 jz import_done ; if 2 stopping byte together 00405988 ; it is the end of Import Data 00405988 ; ------------------------------------------------------ 0040598E 90 90 90 90 90 90+ db 35h dup(90h) 004059C3 ; ------------------------------------------------------ 004059C3 E9 8A F9 FF FF jmp near ptr next_dll_entry
I hope the comments are sufficient. Basically, Import Data is stored in a big block with "FE" used as a stopping byte or marker. Each DLL import entry is started with DLL name in plain ASCII, followed by import entries table and ended with "FE". Each import entry in the table comprises of 5 bytes, the first byte is first ASCII char of API name, the next dword is the API name hash. Here is an example of Import Data block :
00407E21 6B 65 72 6E 65 6C+aKernel32_dll_0 db 'kernel32.dll',0 00407E2E 56 db 56h ; V 00407E2F B5 F2 31 F6 dd 0F631F2B5h 00407E33 56 db 56h ; V 00407E34 22 0A AC 32 dd 32AC0A22h <-------------SNIP------------->
00407E60 47 db 47h ; G 00407E61 E4 0D 06 DE dd 0DE060DE4h 00407E65 FF db 0FEh ; stopping byte 00407E66 75 73 65 72 33 32+aUser32_dll db 'user32.dll',0 00407E71 4D db 4Dh ; M 00407E72 71 A2 D2 A8 dd 0A8D2A271h 00407E76 FF db 0FEh ; stopping byte 00407E77 FF db 0FEh ;
Each import entry above is then passed as parameters for the "home-made"
GetProcAddress which (assumes that system DLL export directories are
alphabetically sorted) will then parse the export directory until it finds a
match on the first char. It will then start performing hash on each API name of
the Export Directory until a match for the required hash is found, this API
address found will then be check for bpx then return to eax by the routine. As
you can see, the compare at 4056CE and the conditional jump at 405745 are key
decider on whether an API should be redirected or not. We would like PELock not
to redirect any API at all but patching the jump wont do simply because the code
CRC is used to calculate our import hash values. There are two solution to this,
patching the "home-made" GetProcAddress to change value of ecx to 0 so that the
jz at 405745 will always not jump or switching the zero flag at 405745 if zero
flag is set. I opted for the second method by using this simple command below in
softice.
bpm 405745 x if ZFL==1 do "r fl z; x"
Softice screen will flicker a lot as the breakpoint is hit pretty often (wonder why :) and there are quite a lot of imports so if you stand up, do a bit of stretching away from the monitor for a minute or so, when you come back PELock will already be running as if nothing had changed. BUT run Imprec now, enter OEP as 1000 and it will automatically find IAT for you at 28000. Click Get Import and Import will be able to resolve all import normally except 1. It is left as an exercise for readers to find out which API it is and why it is still redirected.
4. Decrypting the target EXE
Once you have fixed the IAT redirection routine and obtain a valid Import tree for Imprec, save it and put it aside. It is time to fully decrypt PElock.exe and obtain a full working dump. Run PELock again and launch LordPE, select Partial Dump and enter dump size as PELock.exe image size. Disassemble the dump in IDA you will realize that there are lots of code which are still encrypted and decrypted only when needed. Here is a snippet example of code still encrypted after OEP is reached :
00401ECA E8 33 5A EF FF call near ptr 2F7902h ; CALL Decryptor 00401ECA ; ---------------------------------------------------------- 00401ECF E8 02 00 00 dd 2E8h ; code block property 00401ED3 66 7B 6B 3C 47 89+ db 66h, 7Bh, 6Bh, 3Ch, 47h, 89h, 0EDh, 39h, 53h, 0EFh, 83h, 00401ED3 ED 39 53 EF 83 00+ db 72h, 6Fh, 5Dh, 3Ch, 0EEh, 26h, 0B0h, 78h, 0AEh, 29h, 98h 00401ED3 B7 A1 AD 9D E8 B2+ db 0FBh, 0EBh, 0FAh, 0EBh, 0FCh, 0EBh, 6, 0CDh, 20h, 0EBh, 00401F40 ; ------------------------------------------------------------- 00401F40 E8 F1 61 EF FF call near ptr 2F8136h ; CALL Re-encryptor 00401F40 ; ------------------------------------------------------------- 00401F45 E8 02 00 00 dd 2E8h ; code block property 00401F49 ; -------------------------------------------------------------
As you can see, there are a lot of block of codes as above in our dump.exe and the Decryptor and Re-encryptor address are not within the image of the exe but in PE loader memory instead. Basically, to prevent unpacking and dumping, PELock inserted a lot of these blocks of encrypted code into the original exe, when these codes need to be executed, the Decryptor will be called first, which read the code block property then decrypt the block accordingly, then return from the Decrypt call, execute the code block, call Re-encryptor to re-crypt the code block back again before moving onto normal execution again. Further investigation of dump.exe shows that there are 3 different pairs of Decrypt-Encrypt call with different addresses and different algorithms employed.
Checking these routines in our loader, we will see that the dword below each CALL commented as "code block property" is an encrypted value of the code block length. Though a thorough understanding of Decryptors algorithm is not required to obtain good dump of the target exe, it is useful to go through them to understand roughly how they work. To keep this essay within reasonable length, I will not discussed the algorithm here.
In order to obtain a good dump, we need to force PELock loader to decrypt all these code blocks before reaching OEP. We can patch the Decryptor to decrypt the code blocks and overwrite call to encryptor with simple jump over but how do we know we have decrypted ALL the encrypted blocks left behind after OEP is reached? I was thinking about this problem and decided that PELock loader will need to know where to insert these Call to Decryptor and Re-encryptor as the loader offset will be different in different computer, hence PELock loader will need to calculate the call distance each time it loads up and insert these distances into the target exe so there must be a kind of table that store the offset of where these Calls should be inserted. Indeed that was the case. Re-run PELock and place bpm write one of these calls to Decryptor in the original EXE image and softice will break right in the middle of these routine.
0040DBA8 insert_crypt_engine_3: ; CODE XREF: seg000:00407578p 0040DBA8 60 pusha 0040DBA9 E8 A3 FF FF FF call mov_ebp_40DB56 ; set ebp the necessary base 0040DBAE 8D 75 F7 lea esi, [ebp-9] 0040DBAE ; ----------------------------------------------------------- 0040DBB1 90 90 90 90 90 90+ db 30h dup(90h) 0040DBE1 ; ----------------------------------------------------------- 0040DBE1 8D 95 6C 03 00 00 lea edx, [ebp+36Ch] ; get address of Decryptor 0040DBE1 ; ----------------------------------------------------------- 0040DBE7 90 90 90 90 90 90+ db 21h dup(90h) 0040DC08 ; ----------------------------------------------------------- 0040DC08 B9 03 00 00 00 mov ecx, 3 ; size of table, only 3 entries 0040DC08 ; ----------------------------------------------------------- 0040DC0D 90 90 90 90 90 90+loop_inserting_all_engine_loc db 24h dup(90h) ; 0040DC31 ; ----------------------------------------------------------- 0040DC31 8B 06 mov eax, [esi] ; read one dword from table 0040DC31 ; -------------------; each dword is address where Call are 0040DC33 90 90 90 90 90 90+ db 25h dup(90h) ; to be inserted 0040DC58 ; ----------------------------------------------------------- 0040DC58 83 EE 04 sub esi, 4 ; mov pointer on table backwards 0040DC58 ; ----------------------------------------------------------- 0040DC5B 90 90 90 90 90 90+ db 2Dh dup(90h) 0040DC88 ; ----------------------------------------------------------- 0040DC88 C6 00 E8 mov byte ptr [eax], 0E8h ; set up the CALL byte 0040DC88 ; ----------------------------------------------------------- 0040DC8B 90 90 90 90 90 90+ db 2Dh dup(90h) 0040DCB8 ; ----------------------------------------------------------- 0040DCB8 40 inc eax 0040DCB9 8B FA mov edi, edx ; edi -> address of Decryptor 0040DCBB 2B F8 sub edi, eax ; calculate the Call distance 0040DCBB ; ----------------------------------------------------------- 0040DCBD 90 90 90 90 90 90+ db 2Bh dup(90h) 0040DCE8 ; ----------------------------------------------------------- 0040DCE8 83 EF 04 sub edi, 4 ; minus instruction length 0040DCEB 89 38 mov [eax], edi ; insert CAll distance after E8 0040DCEB ; ----------------------------------------------------------- PATCHES made to force decrypt code
0040DCED 90 90 90 90 90 90+ db 28h dup(90h) ; push dword ptr [eax+4] 0040DCED 90 90 90 90 90 90+ ; pushad 0040DCED 90 90 90 90 90 90+ ; dec eax 0040DCED 90 90 90 90 90 90+ ; call eax=>decrypt routine which is patched 0040DCED 90 90 90 90 90 90+ ; popad 0040DCED 90 90 90 90 90 90+ ; pop dword ptr [eax+4] 0040DCED 90 90 90 90 ; mov word [eax-1], 7EB 0040DD15 ; ----------------------------------------------------------- 0040DD15 83 C0 08 add eax, 8 ; 0040DD18 03 40 FC add eax, [eax-4] ; add code block length 0040DD1B 83 C0 10 add eax, 10h ; eax -> where Call Re-encryptor
should be placed 0040DD1B ; ----------------------------------------------------------- 0040DD1E 90 90 90 90 90 90+ db 2Eh dup(90h) ; Here another call is set up 0040DD4C ; ----------------------------------------------------------- 0040DD4C C6 00 E8 mov byte ptr [eax], 0E8h ; 'F' ; mov byte ptr [eax], E9 0040DD4C ; ----------------------------------------------------------- 0040DD4F 90 90 90 90 90 90+ db 2Fh dup(90h) ; to call Re-encryptor 0040DD7E ; ----------------------------------------------------------- 0040DD7E 40 inc eax 0040DD7E ; ----------------------------------------------------------- 0040DD7F 90 90 90 90 90 90+ db 2Ch dup(90h) ; edi -> address of Re-encryptor 0040DDAB ; ----------------------------------------------------------- 0040DDAB 8D BD 4E 07 00 00 lea edi, [ebp+74Eh] ; lea edi, [eax+8] 0040DDAB ; ----------------------------------------------------------- 0040DDB1 90 90 90 90 90 90+ db 28h dup(90h) 0040DDD9 ; ----------------------------------------------------------- 0040DDD9 2B F8 sub edi, eax ; calculate Call distance 0040DDD9 ; ----------------------------------------------------------- 0040DDDB 90 90 90 90 90 90+ db 23h dup(90h) 0040DDFE ; ----------------------------------------------------------- 0040DDFE 83 EF 04 sub edi, 4 ; minus instruction length 0040DDFE ; ----------------------------------------------------------- 0040DE01 90 90 90 90 90 90+ db 25h dup(90h) 0040DE26 ; ----------------------------------------------------------- 0040DE26 89 38 mov [eax], edi ; insert call distance 0040DE26 ; ----------------------------------------------------------- 0040DE28 90 90 90 90 90 90+ db 2Eh dup(90h) ; to finalise the Call 0040DE56 ; ----------------------------------------------------------- 0040DE56 49 dec ecx ; decrease counter along the table 0040DE56 ; ----------------------------------------------------------- 0040DE57 90 90 90 90 90 90+ db 36h dup(90h) ; if not end of table, loop back 0040DE8D ; ----------------------------------------------------------- 0040DE8D 0F 85 7A FD FF FF jnz near ptr loop_inserting_all_engine_loc 0040DE8D ; -----------------------------------------------------------
0040DE93 90 90 90 90 90 90+ db 23h dup(90h) 0040DEB6 ; ----------------------------------------------------------- 0040DEB6 8D 7D 52 lea edi, [ebp+52h] 0040DEB9 B9 16 03 00 00 mov ecx, 316h ; overwrite code area above 0040DEBE F3 AA repe stosb ; with garbage 0040DEC0 61 popa 0040DEC1 C3 retn
The comments are pretty clear. Address of encrypted code are stored in tables, where each entry is read, then E8xxxxxxxx is placed there to Call Decryptor and location of Call to Re-encryptor is calculated based on the encrypted code block length. We need to patch these routine so that they will decrypt all code blocks listed on the table of addresses and not place Call to Decryptor/Encryptor into these address. I have made the necessary patches in orange as commented above. "Call eax" is used to simulate execution of the original exe and hence will trigger Call Decryptor which will then decrypt the code block following that address. However, we need to patch the Decryptor routine not to return execution to original exe like normal but to return execution to our patched loop. This can be done simply by correcting the stack.
We need to find the end of the Decryptor routine which looks like this
ret ; return execution to just decrypted code block
and patch it to correct stack like this
pop eax ; eax = address of decrypted code block
ret ; return from our "Call eax" to our patched loop
4. The CRC trap
As mentioned above, there are 3 different set of Decryptor/Encryptor and hence each has a separate routine inserting those Calls into the target exe with a separate table containing address where those calls should be inserted to. In order to obtain a good dump, we need to patch all 3 routines to force decrypt the whole code section of the target exe. However, problem arises when one of these Calls Insertion routine has an encrypted table of address which uses the routine CRC to decrypt these table entries. Attempts to patch it will cause table entries to be decrypted incorrectly. Here is how the routine look like :
00408368 Insert_Crypt_Engine_1: ; 00408368 B9 4A 00 00 00 mov ecx, 4Ah ; size of table 1 00408368 ; ----------------------------------------------------------- 0040836D 90 90 90 90 90 90+ db 27h dup(90h) 00408394 ; ----------------------------------------------------------- 00408394 8D 75 E8 lea esi, [ebp-18h] ; start of Table 1 00408394 ; ----------------------------------------------------------- 00408397 90 90 90 90 90 90+ db 27h dup(90h) 004083BE ; ----------------------------------------------------------- 004083BE 8D 95 68 0B 00 00 lea edx, [ebp+0B68h] ; edx -> Decryptor address 004083BE ; ----------------------------------------------------------- 004083C4 90 90 90 90 90 90+get_crypted_offset db 55h dup(90h) ; CODE XREF: ... 00408419 ; ----------------------------------------------------------- 00408419 8B 06 mov eax, [esi] ; read encrypted entry from table 0040841B EB 31 jmp short near ptr prepare_CRC_data_pointer 0040841B ; ----------------------------------------------------------- 0040841D 90 90 90 90 90 90+set_no_of_loop db 27h dup(90h) ; CODE XREF: ... 00408444 ; ----------------------------------------------------------- 00408444 BB 1F 01 00 00 mov ebx, 11Fh 00408449 E9 25 01 00 00 jmp near ptr calculate_insertion_address 00408449 ; ----------------------------------------------------------- 0040844E 90 90 90 90 90 90+prepare_CRC_data_pointer db 2Eh dup(90h) ; CODE XREF: ... 0040847C ; ----------------------------------------------------------- 0040847C 83 EE 04 sub esi, 4 0040847F EB 9C jmp short near ptr set_no_of_loop 0040847F ; ----------------------------------------------------------- 00408481 90 90 90 90 90 90+get_call_distance db 2Fh dup(90h) ; CODE XREF: ... 004084B0 ; ----------------------------------------------------------- 004084B0 2B F8 sub edi, eax 004084B2 E9 84 00 00 00 jmp near ptr minus_length_4_bytes 004084B2 ; ----------------------------------------------------------- 004084B7 90 90 90 90 90 90+set_up_CALL_decrypt db 2Eh dup(90h) ; CODE XREF: ... 004084E5 ; ----------------------------------------------------------- 004084E5 C6 00 E8 mov byte ptr [eax], 0E8h ; 'F' ; set up CALL 004084E8 E9 09 01 00 00 jmp near ptr minus_1_byte 004084E8 ; ----------------------------------------------------------- 004084ED 90 90 90 90 90 90+decrease_counter db 1Bh dup(90h) ; CODE XREF: ... 00408508 ; ----------------------------------------------------------- 00408508 49 dec ecx ; decrease counter to check end of table 00408509 E9 C1 02 00 00 jmp check_end_of_table? 00408509 ; ----------------------------------------------------------- 0040850E 90 90 90 90 90 90+get_CALL_to_address db 26h dup(90h) ; CODE XREF: ... 00408534 ; ----------------------------------------------------------- 00408534 8B FA mov edi, edx 00408536 E9 46 FF FF FF jmp near ptr get_call_distance 00408536 ; ----------------------------------------------------------- 0040853B 90 90 90 90 90 90+minus_length_4_bytes db 30h dup(90h) ; CODE XREF: ... 0040856B ; ----------------------------------------------------------- 0040856B 83 EF 04 sub edi, 4 ; decrease pointer to table entries by 4 0040856E E9 F3 01 00 00 jmp near ptr store_jump_distance 0040856E ; ----------------------------------------------------------- 00408573 90 90 90 90 90 90+calculate_insertion_address db 27h dup(90h) ; CODE XREF: ... 0040859A ; ----------------------------------------------------------- 0040859A CRC_loop: ; CODE XREF: ... 0040859A 03 84 9D F6 05 00+ add eax, [ebp+ebx*4+5F6h] 004085A1 D1 C8 ror eax, 1 ; loop calculating the address 004085A3 4B dec ebx ; where CALL to decrypt code 004085A4 75 F4 jnz short CRC_loop ; can be inserted to 004085A6 E9 0C FF FF FF jmp near ptr set_up_CALL_decrypt ; <=== HERE do bpm 004085A6 ; ----------------------------------------------------------- 004085AB 90 90 90 90 90 90+get_distance_call db 44h dup(90h) ; CODE XREF: ... 004085EF ; ----------------------------------------------------------- 004085EF 2B F8 sub edi, eax 004085F1 E9 E1 01 00 00 jmp near ptr byte_4087D7 004085F1 ; ----------------------------------------------------------- 004085F6 90 90 90 90 90 90+minus_1_byte db 29h dup(90h) ; CODE XREF: ... 0040861F ; ----------------------------------------------------------- 0040861F 40 inc eax 00408620 E9 E9 FE FF FF jmp near ptr get_CALL_to_address 00408620 ; ----------------------------------------------------------- 00408625 90 90 90 90 90 90+byte_408625 db 24h dup(90h) ; CODE XREF: ... 00408649 ; ----------------------------------------------------------- 00408649 89 38 mov [eax], edi 0040864B E9 9D FE FF FF jmp near ptr decrease_counter 0040864B ; ----------------------------------------------------------- 00408650 90 90 90 90 90 90+calculate_block_length db 1Eh dup(90h) ; CODE XREF: ... 0040866E ; ----------------------------------------------------------- 0040866E 83 C0 08 add eax, 8 ; points to encrypted code 00408671 EB 00 jmp short $+2 00408673 8B 58 FC mov ebx, [eax-4] ; get crypted_length 00408673 ; ----------------------------------------------------------- 00408676 90 90 90 90 90 90+ db 2Eh dup(90h) 004086A4 ; ----------------------------------------------------------- 004086A4 C1 CB 03 ror ebx, 3 ; ror ebx, 3 004086A4 ; ----------------------------------------------------------- 004086A7 90 90 90 90 90 90+ db 28h dup(90h) 004086CF ; ----------------------------------------------------------- 004086CF 03 C3 add eax, ebx ; eax -> where to insert Call Encryptor 004086D1 E9 C1 00 00 00 jmp near ptr get_end_pointer 004086D1 ; ----------------------------------------------------------- 004086D6 90 90 90 90 90 90+get_reencrypt_call_address db 26h dup(90h) ; CODE XREF: ... 004086FC ; ----------------------------------------------------------- 004086FC 8D BD 9C 13 00 00 lea edi, [ebp+139Ch] 00408702 E9 A4 FE FF FF jmp near ptr get_distance_call 00408702 ; ----------------------------------------------------------- 00408707 90 90 90 90 90 90+insert_encrypt_call db 2Dh dup(90h) ; CODE XREF: ... 00408734 ; ----------------------------------------------------------- 00408734 C6 00 E8 mov byte ptr [eax], 0E8h ; 'F' 00408737 EB 00 jmp short $+2 00408737 ; ----------------------------------------------------------- 00408739 90 90 90 90 90 90+ db 27h dup(90h) 00408760 ; ----------------------------------------------------------- 00408760 40 inc eax 00408761 E9 70 FF FF FF jmp near ptr get_reencrypt_call_address 00408761 ; ----------------------------------------------------------- 00408766 90 90 90 90 90 90+store_jump_distance db 2Ah dup(90h) ; CODE XREF: ... 00408790 ; ----------------------------------------------------------- 00408790 89 38 mov [eax], edi 00408792 E9 B9 FE FF FF jmp near ptr calculate_block_length 00408792 ; ----------------------------------------------------------- 00408797 90 90 90 90 90 90+get_end_pointer db 30h dup(90h) ; CODE XREF: ... 004087C7 ; ----------------------------------------------------------- 004087C7 83 C0 10 add eax, 10h 004087CA E9 38 FF FF FF jmp near ptr insert_encrypt_call 004087CF ; ----------------------------------------------------------- 004087CF check_end_of_table?: ; CODE XREF: ... 004087CF 0F 85 1C FC FF FF jnz near ptr get_crypted_offset+2Dh ; loop if not finished 004087D5 EB 3C jmp short _done_ ; 004087D5 ; ----------------------------------------------------------- 004087D7 90 90 90 90 90 90+byte_4087D7 db 34h dup(90h) ; CODE XREF: ... 0040880B ; ----------------------------------------------------------- 0040880B 83 EF 04 sub edi, 4 0040880E E9 12 FE FF FF jmp near ptr byte_408625 00408813 ; ----------------------------------------------------------- 00408813 _done_: ; bpm here to wait till it finishes
I know this is hard to follow because the code flow is not linear but hey it is not my fault, it was the intention of PELock author to make our lives harder, I hope the coloring helps a bit. The algorithm is similar to the insertion algo in the previous code snippet, only differences are code block length calculation. and tables entries are decrypted on the fly. Basically the decryption of table entries to addresses of encrypted code block is done by the CRC_loop at 40859A. If you place a breakpoint at 4085A6 you will see that the decrypted address is stored in eax. Here I can think of two solutions to overcome CRC check, either we copy the whole original block of code into another memory location and patch the CRC routine to perform CRC check on that new location or we can again use softice breakpoints features. I opted for the latter, though your softice screen will flicker a lot, it is simpler to perform.
Basically, I let PELock loader goes on executing as normal, decrypt each table
entry then insert the appropriate calls in front and after the encrypted code
blocks. However, I will place a breakpoint at 4085A6 and store the decrypted
addresses into the table so we will at least have a decrypted table in the end.
bpm 408813 x ; the ending breakpoint
for us when the whole insertion loop is finished!
bpm 4085A6 x do "dd esi+4 eax; x" ; store decrypted value eax
back into table
; esi pointer is already decremented by 4 so we have plus 4
Once softice screen stop flickering and our final breakpoint is reached, you
will see in data window of softice our newly decrypted table of address of
encrypted code block. All we have left to do is then insert some code by softice
window to force decrypt all the codes block listed in this last table.
a eip
mov ecx, 4A ; size of table load_ lodsd ; read 1 entry from table push dword ptr [eax+5] ; preserve code block property values pushad call eax ; simulate original exe execution popad pop dword ptr [eax+5] ; restore code block property values mov word ptr [eax], 7EB ; patch Call Decryptor with a Jump Over mov ebx, word ptr [eax+5] ; read code block property ror ebx, 3 ; get code block length add eax, ebx add eax, 19 ; calculate address of Call Encryptor mov word ptr [eax], 7EB ; replace Call Encryptor with a Jump Over dec ecx ; decrease size of table test ecx, ecx jne load_ ; get next entry of table if not done jmp eip ; infinite loop, DUMP here
Now, this procedure described above should be done last, after you have patched the other two insertion routine do force decrypt those code blocks listed in those two corresponding tables. This is so that when we let PELock run again and reach our infinite jump, ALL the encrypted codes block inside target EXE are fully decrypted and Calls to Decryptors and Re-encryptors and patched with simple Jumps Over.
5. Finding OEP
Now that we have obtained a good dump with all codes decrypted and fixed, it is time to run Imprec again and fix the Import Directory of our dump. The final task is of course finding OEP which is relatively simple for PElock. Looking at the disassembly and hex display of our dump.exe, we see that the first section is started with a lot of strings then followed by code so we can simply guess that OEP is right at the end of those strings.
Another and more generic of finding OEP in PELock protected apps would be finding where PELock jumps to OEP. This occurs soon after the three Insertion Routine above are performed to insert those Calls to Decryptors and Encryptors into the target exe.
00407578 E8 2B 66 00 00 call insert_crypt_engine_3 ; our insertion routine call 00407578 ; ----------------------------------------------------------- 0040757D 90 90 90 90 90 90+ db 28h dup(90h) ; that is responsible for inserting 004075A5 ; ----------------------------------------------------------- 004075A5 E8 0C 73 00 00 call insert_crypt_engine_2 ; those Call Decryptor/Encryptor 004075A5 ; ----------------------------------------------------------- 004075AA 90 90 90 90 90 90+ db 28h dup(90h) ; into the target exe main code section 004075D2 ; ----------------------------------------------------------- 004075D2 E8 C5 07 00 00 call unknown_import_stuff_and_insert_crypt_engine_1 004075D2 ; ----------------------------------------------------------- 004075D7 90 90 90 90 90 90+ db 28h dup(90h) ; these code would still be encrypted <-------------SNIP--------------> ; and decrypted last right before reaching 00407AA1 5D pop ebp ; OEP. 00407AA1 ; ----------------------------------------------------------- 00407AA2 90 90 90 90 90 90+ db 124h dup(90h) 00407BC6 ; ----------------------------------------------------------- 00407BC6 68 1E 1D 40 00 push 401D1Eh ; OEP!!! 00407BC6 ; ----------------------------------------------------------- 00407BCB 90 90 90 90 90 90+ db 23h dup(90h) 00407BEE ; ----------------------------------------------------------- 00407BEE C3 retn
That is it, phew. Hence once you find any of the Insertion Routine, you can then check on stack to find where they are called from and place the appropriate bpm. Of course you should dump the loader there and look inside IDA for better view. From then onwards OEP, there would be simple loop decryption and SEH to prevent manual tracing but with IDA I am sure you can find the last bit of pushing OEP as above :).
6. Final thought
WOW, a hell of a protector. There are actually more things I wish to discuss about but this tutorial has become too long and I am sure if you reach here, you would not wanna read any more of my words. I am also tired of writing so maybe another time.
Overall, PELock is a pretty good protection and it was really fun reversing it. If PELock has inserted those Calls to Decryptor and Encryptor at packing time and removed the tables containing addresses of encrypted code block, it would be much harder to decrypt ALL code blocks as we cant be so sure if we have decrypt all blocks. There would be a problem because the loader will always be loaded at different offset so PElock would know the Call distance at packing time but she =) can always uses CALL dword ptr [xxxxxxxx] instead where xxxxxxx is some fixed address. Ah well, just an idea.
I hope you enjoy the tutorial. Again, comments are welcome and if you feel that I should improve or correct certain things about this tutorial, feel free me drop me a message on Fravia Message Board.
--------------------------------------------------------
This tutorial would not have been possible without the help of all my friends
and previous tutorials writers.
Last Edited : 30 Jan 2003
Cheers,
crUsAdEr